package it.unibo.refolding.alg;

import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.common.MyRuntimeException;
import it.unimi.dsi.fastutil.shorts.ShortArrayList;
import it.unimi.dsi.fastutil.shorts.ShortList;

import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.commons.math3.analysis.UnivariateFunction;
import org.apache.commons.math3.analysis.interpolation.LinearInterpolator;

import com.google.common.base.Preconditions;

public class NoisyFunction {

    public static final NoisyFunction EMPTY = new NoisyFunction(null, new short[0]) {

        @Override
        public double value(short x) {
            return Double.NaN;
        }

        @Override
        public double distance(NoisyFunction other) {
            return Double.POSITIVE_INFINITY;
        }
    };
    
    private final UnivariateFunction f;
    private final short[] keys;

    public NoisyFunction(UnivariateFunction f, short[] keys) {
        Preconditions.checkArgument(CommonThings.increasing(keys));
        this.f = f;
        this.keys = keys;
    }

    public double value(short x) {
        try {
            return f.value(x);
        } catch (IllegalArgumentException e) {
            throw new MyRuntimeException(e.toString(), e);
        }
    }
    
    /**
     * Two functions with disjunct domains will have a distance of zero.
     */
    public double distance(NoisyFunction other) {
        short[] a = CommonThings.merge(keys, other.keys);
        int n = CommonThings.uniq(a);
        double sum = 0;
        int count = 0;
        for (int i = 0; i < n; ++i) {
            short x = a[i];
            if (inDomain(x) & other.inDomain(x)) {
                double diff = value(x) - other.value(x);
                sum += diff * diff;
                ++count;
            }
        }
        return count == 0 ? 0 : Math.sqrt(sum / count);
    }
    
    private boolean inDomain(short x) {
        return lowerDomainBound() <= x & x <= upperDomainBound();
    }

    private short lowerDomainBound() {
        return keys[0];
    }
    
    private short upperDomainBound() {
        return keys[keys.length - 1];
    }

    public static NoisyFunction of(short[] x, short[] y) {
        Preconditions.checkArgument(x.length == y.length);
        SortedMap<Short, ShortList> map = new TreeMap<>();
        for (int i = 0; i < x.length; ++i) {
            ShortList v = map.get(x[i]);
            if (v == null) {
                v = new ShortArrayList(2);
                ShortList old = map.put(x[i], v);
                assert old == null;
                v.add(y[i]);
            }
        }
        final short[] keys = new short[map.size()];
        final double[] values = new double[map.size()];
        int i = 0;
        for (Map.Entry<Short, ShortList> entry : map.entrySet()) {
            keys[i] = entry.getKey();
            values[i++] = CommonThings.median(entry.getValue().toShortArray());
        }
        final UnivariateFunction f;
        if (keys.length == 1) {
            f = new UnivariateFunction() {
                @Override
                public double value(double x) {
                    Preconditions.checkArgument(x == keys[0]);
                    return values[0];
                }
            };
        } else {
            // Could use pretty much any interpolator here, e.g. LoessInterpolator.
            LinearInterpolator interpolator = new LinearInterpolator();
            f = interpolator.interpolate(CommonThings.toDoubleArray(keys), values);
        }
        return new NoisyFunction(f, keys);
    }
}
